/******************************************************************************
	This file is a part of the Snapshot Bioinformatics Project

	Copyright (C) 2012-2013 Jarom Schow

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

#include "FileUtil.h"

#include <assert.h>

#include <QtCore/QRegExp>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QDebug>

#include "GzFile.h"

using namespace Bio;

const qint64	c_bufferSize = 65536;
const qint64	c_tarBlockSize = 512;
const QRegExp	c_pathSeparator( "[/\\\\]" );

const int	c_tarFilenameLen = 100;
const int	c_tarModeLen = 8;
const int	c_tarUIDLen = 8;
const int	c_tarGIDLen = 8;
const int	c_tarSizeLen = 12;
const int	c_tarMTimeLen = 12;
const int	c_tarChkSumLen = 8;
const int	c_tarLinkLen = 1;
const int	c_tarLinknameLen = 100;

const int	c_tarChkSumOffset = 148;

/**
**/
QString FileUtil::getFilename( const QString &filepath )
{
	int	idx = filepath.lastIndexOf( c_pathSeparator );

	if( idx < 0 )
		return filepath;

	return filepath.mid( idx + 1 );
}

/**
**/
QString FileUtil::getBaseFilename( const QString &filepath )
{
	QString	filename = getFilename( filepath );
	int		idx = filename.lastIndexOf( '.' );

	if( idx < 0 )
		return filename;

	return filename.left( idx );
}

/**
**/
QString FileUtil::getExtension( const QString &filepath )
{
	int	sep = filepath.lastIndexOf( c_pathSeparator );
	int	idx = filepath.lastIndexOf( '.', sep );

	if( idx < 0 )
		return QString::null;

	return filepath.mid( idx + 1 );
}

/**
**/
QString FileUtil::getPath( const QString &filepath )
{
	int	idx = filepath.lastIndexOf( c_pathSeparator );

	if( idx < 0 )
		return QString::null;

	return filepath.left( idx );
}

/**
**/
QString FileUtil::changeExtension( const QString &filepath, const QString &ext )
{
	int	sep = filepath.lastIndexOf( c_pathSeparator );
	int	idx = filepath.lastIndexOf( '.', sep );

	if( idx < 0 )
		return filepath + "." + ext;

	return filepath.left( idx + 1 ) + ext;
}

/**
**/
bool FileUtil::expandGZFile( const QString &filename, const QString &target )
{
	GzFile	infile( filename );

	if( !infile.open( QIODevice::ReadOnly ) ){
		qWarning() << "Could not open file for reading: " << filename;
		return false;
	}

	QString	tgtFilename( target );

	if( tgtFilename.isEmpty() )
		tgtFilename = getBaseFilename( filename );

	QFile	outfile( tgtFilename );

	if( !outfile.open( QIODevice::WriteOnly ) ){
		qWarning() << "Could not open file for writing: " << tgtFilename;
		infile.close();
		return false;
	}

	bool	status = true;
	char	*buffer = new char[c_bufferSize];

	while( !infile.atEnd() ){
		qint64	num = infile.read( buffer, c_bufferSize );

		if( num < 0 ){
			status = false;
			qWarning() << "Error reading from file: " << filename;
			break;
		}

		if( outfile.write( buffer, num ) < 0 ){
			status = false;
			qWarning() << "Error writing to file: " << tgtFilename;
			break;
		}
	}

	infile.close();
	outfile.close();

	delete buffer;

	return status;
}

/**
**/
bool FileUtil::compressGZFile( const QString &filename, const QString &target )
{
	QFile	infile( filename );

	if( !infile.open( QIODevice::ReadOnly ) ){
		qWarning() << "Could not open file for reading: " << filename;
		return false;
	}

	QString	tgtFilename( target );

	if( tgtFilename.isEmpty() )
		tgtFilename = filename + ".gz";

	GzFile	outfile( tgtFilename );

	if( !outfile.open( QIODevice::WriteOnly ) ){
		qWarning() << "Could not open file for writing: " << tgtFilename;
		infile.close();
		return false;
	}

	bool	status = true;
	char	*buffer = new char[c_bufferSize];

	while( !infile.atEnd() ){
		qint64	num = infile.read( buffer, c_bufferSize );

		if( num < 0 ){
			status = false;
			qWarning() << "Error reading from file: " << filename;
			break;
		}

		if( outfile.write( buffer, num ) < 0 ){
			status = false;
			qWarning() << "Error writing to file: " << tgtFilename;
			break;
		}
	}

	infile.close();
	outfile.close();

	delete buffer;

	return status;
}

/**
**/
bool FileUtil::extractTarFile( const QString &filename, const QString &path, bool expandGZ )
{
	QIODevice	*in = NULL;

	if( expandGZ )
		in = new GzFile( filename );
	else
		in = new QFile( filename );

	if( !in->open( QIODevice::ReadOnly ) ){
		qWarning() << "Error opening file: " << filename;
		delete in;
		return false;
	}

	QString	basePath( path );
	
	if( basePath.isEmpty() )
		basePath = getPath( filename ) + getBaseFilename( filename );

	bool	status = extractTarFile( in, basePath );

	in->close();
	delete in;

	return status;
}

/**
**/
bool FileUtil::createTarFile( const QString &path, const QString &target, bool compressGZ )
{
	QStringList	files = getAllFiles( path );
	QString		filename( target );

	if( filename.isEmpty() ){
		filename = path + ".tar";
		if( compressGZ )
			filename = filename + ".gz";
	}

	return createTarFile( files, filename, compressGZ );
}

/**
**/
bool FileUtil::createTarFile( const QStringList &files, const QString &target, bool compressGZ )
{
	QIODevice	*out = NULL;

	if( compressGZ )
		out = new GzFile( target );
	else
		out = new QFile( target );

	if( !out->open( QIODevice::WriteOnly ) ){
		qWarning() << "Error opening file: " << target;
		delete out;
		return false;
	}

	bool	status = createTarFile( files, out );

	out->close();
	delete out;

	return status;
}

/**
**/
bool FileUtil::extractTarFile( QIODevice *input, const QString &path )
{
	if( !input ){
		qWarning() << "NULL pointer passed to FileUtil::extractTarFile()";
		return false;
	}

	bool	status = true;
	QString	dirname( path );

	if( dirname.isEmpty() )
		dirname = ".";
	else
		dirname = dirname.replace( '\\', '/' );

	if( !dirname.endsWith( '/' ) )
		dirname += '/';

	while( !input->atEnd() ){
		if( !extractFileFromTar( input, dirname ) ){
			status = false;
			break;
		}
	}

	return status;
}

/**
**/
bool FileUtil::createTarFile( const QString &path, QIODevice *output )
{
	QStringList	files = getAllFiles( path );

	return createTarFile( files, output );
}

/**
**/
bool FileUtil::createTarFile( const QStringList &files, QIODevice *output )
{
	if( !output ){
		qWarning() << "NULL pointer passed to FileUtil::createTarFile()";
		return false;
	}

	bool	status = true;

	QStringListIterator	fileIt( files );

	while( fileIt.hasNext() ){
		if( !addFileToTar( fileIt.next(), output ) ){
			status = false;
			break;
		}
	}

	char	buffer[c_tarBlockSize];

	// write two trailing blocks of zeros
	memset( buffer, 0, c_tarBlockSize );
	output->write( buffer, c_tarBlockSize );
	output->write( buffer, c_tarBlockSize );

	return status;
}

/**
**/
QStringList FileUtil::getAllFiles( const QString &path )
{
	QString	dirname( path );

	if( dirname.isEmpty() )
		dirname = ".";
	else
		dirname = dirname.replace( '\\', '/' );

	if( !dirname.endsWith( '/' ) )
		dirname += '/';

	QDir		dir( path );
	QStringList	files;
	QStringList	curFiles = dir.entryList( QDir::Files );

	QStringListIterator	curFileIt( curFiles );

	while( curFileIt.hasNext() )
		files.append( dirname + curFileIt.next() );
	
	QStringList	subDirs = dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot );

	QStringListIterator	subDirIt( subDirs );

	while( subDirIt.hasNext() )
		files.append( getAllFiles( dirname + subDirIt.next() ) );

	return files;
}

/**
**/
bool FileUtil::extractFileFromTar( QIODevice *input, const QString &path )
{
	char	buffer[c_tarBlockSize];
	char	*curPos = buffer;

	// read header
	if( input->read( buffer, c_tarBlockSize ) != c_tarBlockSize ){
		qWarning() << "Unexpected end of .tar file";
		return false;
	}

	if( buffer[0] == 0 ){
		for( int i = 1; i < c_tarBlockSize; i++ ){
			if( buffer[i] != 0 ){
				qWarning() << "Unexpected character in empty block";
				return false;
			}
		}

		return true; // empty block
	}

	QString	filename = path + readTarString( curPos, c_tarFilenameLen );
	int		mode = readTarInt( curPos, c_tarModeLen );
	int		uid = readTarInt( curPos, c_tarUIDLen );
	int		gid = readTarInt( curPos, c_tarGIDLen );
	qint64	size = readTarInt( curPos, c_tarSizeLen );
	int		mtime = readTarInt( curPos, c_tarMTimeLen );
	int		chksum = readTarInt( curPos, c_tarChkSumLen );
	char	link = *(curPos++);
	QString	linkname = readTarString( curPos, c_tarLinknameLen );
	QFile	file( filename );

	if( !file.open( QIODevice::WriteOnly ) ){
		QDir	dir;

		dir.mkpath( getPath( filename ) );

		if( !file.open( QIODevice::WriteOnly ) ){
			qWarning() << "could not open file: " << filename;
			return false;
		}
	}

	while( size > 0 ){
		if( input->read( buffer, c_tarBlockSize ) != c_tarBlockSize ){
			qWarning() << "Unexpected end of .tar file";
			return false;
		}

		if( size > c_tarBlockSize )
			file.write( buffer, c_tarBlockSize );
		else
			file.write( buffer, size );

		size -= c_tarBlockSize;
	}

	file.close();

	return true;
}

/**
**/
bool FileUtil::addFileToTar( const QString &filename, QIODevice *dev )
{
	assert( dev );
	if( !dev ){
		qWarning() << "Null pointer passed to FileUtil::addFileToTar()";
		return false;
	}

	QFile	file( filename );

	if( !file.open( QIODevice::ReadOnly ) ){
		qWarning() << "Failed to open file for reading: " << filename;
		return false;
	}

	qint64	size = file.size();
	QString	fname( filename );

	if( fname.length() >= c_tarFilenameLen )
		fname = getFilename( fname );

	char	buffer[c_tarBlockSize];
	char	*curPos = buffer;

	memset( buffer, 0, c_tarBlockSize );

	writeTarString( fname, curPos, c_tarFilenameLen );	// set the filename field
	writeTarInt( 0100777, curPos, c_tarModeLen );		// set permissions field
	writeTarInt( 0, curPos, c_tarUIDLen );				// set uid field
	writeTarInt( 0, curPos, c_tarGIDLen );				// set gid field
	writeTarInt( int( size ), curPos, c_tarSizeLen );	// set size field
	writeTarInt( 0, curPos, c_tarMTimeLen );			// set modified time field

	// set checksum field to spaces (for now)
	for( int i = 0; i < c_tarChkSumLen; i++ )
		*(curPos++) = ' ';

	*(curPos++) = '0'; // write link indicator - 1 byte	

	// calculate checksum
	int	checksum = 0;
	for( int i = 0; i < c_tarBlockSize; i++ )
		checksum += buffer[i];

	curPos = buffer + c_tarChkSumOffset;				// rewind curPos to checksum field
	writeTarInt( checksum, curPos, c_tarChkSumLen );	// set checksum field

	// write the header to file
	dev->write( buffer, c_tarBlockSize );

	qint64	bytesRead = c_tarBlockSize;

	while( !file.atEnd() ){
		bytesRead = file.read( buffer, c_tarBlockSize );
		dev->write( buffer, bytesRead );
	}

	for( qint64 i = bytesRead; i < c_tarBlockSize; i++ )
		dev->putChar( 0 ); // pad out the block to the end with 0's

	file.close();

	return true;
}

/**
**/
QString FileUtil::readTarString( char *&buff, int fieldLength )
{
	QString	str;

	str.reserve( fieldLength );

	while( fieldLength > 0 ){
		if( *buff == 0 )
			break; // end of string

		str += *(buff++);
		fieldLength--;
	}

	buff += fieldLength;

	return str;
}

/**
**/
int FileUtil::readTarInt( char *&buff, int fieldLength )
{
	int	val = 0;

	// skip spaces
	while( fieldLength > 0 ){
		if( *buff != ' ' )
			break;
		buff++;
		fieldLength--;
	}

	// read digits
	while( fieldLength > 0 ){
		if( *buff < '0' || *buff > '9' )
			break; // end of digits

		val *= 8;
		val += *(buff++) - '0';
		fieldLength--;
	}

	buff += fieldLength;

	return val;
}

/**
**/
void FileUtil::writeTarString( const QString &str, char *&buf, int fieldLength )
{
	int	len = str.length();

	if( len > fieldLength )
		len = fieldLength;

	// write the string
	for( int i = 0; i < len; i++ )
		*(buf++) = str[i].toLatin1();

	// pad the rest of the field with 0's
	for( int i = len; i < fieldLength; i++ )
		*(buf++) = 0;
}

/**
**/
void FileUtil::writeTarInt( int val, char *&buf, int fieldLength )
{
	buf[fieldLength - 1] = 0;

	// write the integer backwards in octal - padding the front with 0's
	for( int i = fieldLength - 2; i >= 0; i-- ){
		buf[i] = char( val % 8 ) + '0';
		val /= 8;
	}

	buf += fieldLength;
}
